﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Permissions;
using System.Text;
using System.Text.RegularExpressions;

namespace ChangeText
{
    internal class Program
    {
        private static string _insertedKey;
        private static readonly StringBuilder Output=new StringBuilder();

        /// <summary>
        /// Main entry point
        /// </summary>
        /// <param name="args"></param>
        private static void Main(string[] args)
        {
            MainWork(args);
        }

        private static void MainWork(string[] args)
        {
            if (args.Any() && !args.Any(a => a.Equals("/help")))
            {

                Output.AppendLine("\n============================= Working =============================\n");
                try
                {
                    int multipleFilesDetector;

                    //parse Attributes
                    var attributes = ParseAttributes(args.ToList(), out multipleFilesDetector);

                    //replacing Text
                    if (multipleFilesDetector == 0)
                    {
                        FindAndReplaceInFile(attributes);
                    }
                    else
                    {
                        FindAndReplaceInList(attributes);
                    }
                }
                catch (ArgumentException ex)
                {
                    Output.AppendLine(ex.Message);
                    HelpToConsole();
                }
                catch (Exception ex)
                {
                    Output.AppendLine("Something wrong. See details below:\n");
                    Output.AppendLine(ex.Message.SplitForConsole());
                    HelpToConsole();
                }
            }
            else
            {
                HelpToConsole();
            }
            Output.AppendLine("\nEnter [Q] to exit, [/help] for help or correct attributes set for new try... \n");
            Console.Title = "Changing text file";
            Console.Clear();
            var regex = new Regex("\\n");
            Console.BufferHeight = regex.Matches(Output.ToString()).Count;
            Console.Write(Output.ToString());
            Console.Write("Input: ");
            _insertedKey = Console.ReadLine();
            Console.WriteLine();
            if (string.IsNullOrEmpty(_insertedKey) || !_insertedKey.Equals("q", StringComparison.CurrentCultureIgnoreCase))
            {
                Output.Clear();
                args = !string.IsNullOrEmpty(_insertedKey) ? _insertedKey.Split(' ') : args;
                MainWork(args);
            }
        }

        private static void HelpToConsole()
        {
            Output.AppendLine("\n============================= Usage =============================");
            Output.AppendLine(" /path [path] /find [find] /replace [replace] {/encoding [encoding]} \nOR\n " +
                              " /list [list] /find [find] /replace [replace] {/encoding [encoding]}\n" +
                              "where:\n"+
                              "[path] - fully qualified path to file to be changed\n" +
                              "[find] - string to be replaced\n" +
                              "[replace] - string to replace with\n" +
                              "[encoding] - optional. Specify, if files aren't using Unicode encoding.\n" +
                              "Example: for Cyrillic characters it may be /encoding \"Windows-1251\"\n" +
                              "[list of files] - fully qualified path to file with paths to documents to\n" +
                              " change text in (each from new line). Example:\n" +
                              "*******************************\n" +
                              "c:\\FirstDocumentToChange.txt\n" +
                              "c:\\SecondDocumentToChange.txt\n" +
                              "*******************************\n" +
                              "!!! Use quotes if any whitespace in path/text exists !!!\n" +
                              "!!! Backslash is the escape character !!!\n" +
                              "!!! Always escape quotes !!!\n" +
                              "!!! Only escape backslashes if they precede a quote !!!");
            Output.AppendLine("=================================================================\n");
        }

        /// <summary>
        /// Replaces text in single file
        /// </summary>
        /// <param name="attributes"></param>
        private static void FindAndReplaceInFile(Attributes attributes)
        {
            // check existence
            if (!File.Exists(attributes.FileOrListPath))
            {
                throw new InvalidOperationException(string.Format("File not exists. Check file path \r\n'{0}'.",
                    attributes.FileOrListPath));
            }
            // check access permissions
            new FileIOPermission(FileIOPermissionAccess.Read | FileIOPermissionAccess.Write, attributes.FileOrListPath)
                .Demand();
            // read file data
            int numberOfChanges;
            Encoding encoding = string.IsNullOrEmpty(attributes.Encoding)
                ? Encoding.Default
                : Encoding.GetEncoding(attributes.Encoding);
                List<string> data = File.ReadAllLines(attributes.FileOrListPath,encoding).ToList();
                UpdateData(data, attributes.TextToFind, attributes.TextToReplace, out numberOfChanges);
                
            if (numberOfChanges > 0)
            {
                File.WriteAllLines(attributes.FileOrListPath, data, encoding);
            }
            Output.AppendLine(String.Format("{0} lines changed in file:\n", numberOfChanges));
            Output.AppendLine(
                String.Format("{0,5}{1}\n", " ", attributes.FileOrListPath).SplitForConsole(new List<char> {'\\', '/'}));
        }

        /// <summary>
        /// Replaces text in multiple files
        /// </summary>
        /// <param name="attributes"></param>
        private static void FindAndReplaceInList(Attributes attributes)
        {
            // check existence
            if (!File.Exists(attributes.FileOrListPath))
            {
                throw new InvalidOperationException("File with list not exists. Check file path.");
            }
            // check access permissions
            new FileIOPermission(FileIOPermissionAccess.Read, attributes.FileOrListPath).Demand();
            // read file data
            var data = File.ReadAllLines(attributes.FileOrListPath);
            foreach (string path in data)
            {
                try
                {
                    var eachFileInList = new Attributes(path, attributes.TextToFind, attributes.TextToReplace,attributes.Encoding);
                    FindAndReplaceInFile(eachFileInList);
                }
                catch (Exception ex)
                {
                    Output.AppendLine(string.Concat(ex.Message.SplitForConsole(),"/n"));
                }
            }
        }

        /// <summary>
        /// Updates data
        /// </summary>
        /// <param name="data"></param>
        /// <param name="textToFind"></param>
        /// <param name="textToReplace"></param>
        /// <param name="numberOfChanges"></param>
        private static void UpdateData(List<string> data, string textToFind, string textToReplace,
            out int numberOfChanges)
        {
            numberOfChanges = 0;
            for (int i = 0; i < data.Count; i++)
            {
                if (data[i].Contains(textToFind))
                {
                    data[i] = data[i].Replace(textToFind, textToReplace);
                    numberOfChanges += 1;
                }
            }
        }

        /// <summary>
        /// Parses attributes
        /// </summary>
        /// <param name="args"></param>
        /// <param name="multipleFilesDetector"></param>
        /// <returns></returns>
        /// <exception cref="ArgumentException">Invalid arguments</exception>
        private static Attributes ParseAttributes(List<string> args, out int multipleFilesDetector)
        {
            int indexFilePath = args.IndexOf("/path") + 1;
            multipleFilesDetector = 0;
            if (indexFilePath == 0)
            {
                indexFilePath = args.IndexOf("/list") + 1;
                multipleFilesDetector = 1;
            }
            int indexFind = args.IndexOf("/find") + 1;
            int indexReplace = args.IndexOf("/replace") + 1;
            int indexEncoding = args.IndexOf("/encoding") + 1;
            string filePath;
            string textToFind;
            string textToReplace;
            string encoding;

            SetAttributesValues(args, indexFilePath, indexFind, indexReplace, indexEncoding, out filePath, out textToFind, out textToReplace, out encoding);

            if ((new[] { indexFilePath, indexFind, indexReplace }.All(x => x != 0)) &&
                new[] { filePath, textToFind }.All(x => !string.IsNullOrEmpty(x)))
            {
                return new Attributes(filePath, textToFind, textToReplace,encoding);
            }
            var sb = new StringBuilder("\n\n     ");
            var argsSb = new StringBuilder();
            foreach (var arg in args)
            {
                argsSb.Append(arg + " ");
            }
            sb.Append(argsSb.ToString().SplitForConsole(new List<char> {' ', '\\', '-', '_'}));
            ExtendedErrorDescriptionForException(sb, indexFilePath, indexFind, indexReplace, filePath, textToFind);

            throw new ArgumentException("Invalid arguments provided." + sb);
        }

        private static void SetAttributesValues(List<string> args, int indexFilePath, int indexFind, int indexReplace,
            int indexEncoding, out string filePath, out string textToFind, out string textToReplace, out string encoding)
        {
            try
            {
                filePath = args[indexFilePath];
            }
            catch (Exception)
            {
                filePath = null;
            }
            try
            {
                textToFind = args[indexFind];
            }
            catch (Exception)
            {
                textToFind = null;
            }
            try
            {
                textToReplace = args[indexReplace];
            }
            catch (Exception)
            {
                textToReplace = null;
            }
            try
            {
                encoding = indexEncoding == 0 ? string.Empty : args[indexEncoding];
                
            }
            catch (Exception)
            {
                encoding = null;
            }
        }

        private static void ExtendedErrorDescriptionForException(StringBuilder sb, int indexFilePath, int indexFind,
            int indexReplace, string filePath, string textToFind)
        {
            sb.Append("\n\n");
            var alreadyHasError = false;
            if (indexFilePath == 0)
            {
                sb.Append("== /path or /list key should be specified ==");
                alreadyHasError = true;
            }
            if (string.IsNullOrEmpty(filePath))
            {
                if (alreadyHasError) sb.Append("\n");
                sb.Append("== /path or /list key should have value ==");
                alreadyHasError = true;
            }
            if (indexFind == 0)
            {
                if (alreadyHasError) sb.Append("\n");
                sb.Append("== /find key should be specified ==");
                alreadyHasError = true;
            }
            if (string.IsNullOrEmpty(textToFind))
            {
                if (alreadyHasError) sb.Append("\n");
                sb.Append("== /find key should have value ==");
                alreadyHasError = true;
            }
            if (indexReplace == 0)
            {
                if (alreadyHasError) sb.Append("\n");
                sb.Append("== /replace key should be specified ==");
            }

        }
    }
}
